Skip to content

feat(tree): Add schema-agnostic tree traversal APIs #24723

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 96 commits into from
Jun 17, 2025

Conversation

Josmithr
Copy link
Contributor

Adds child and children methods to treeNodeApi.

@Josmithr Josmithr requested a review from CraigMacomber May 28, 2025 20:08
@github-actions github-actions bot added area: dds Issues related to distributed data structures area: dds: tree public api change Changes to a public API base: main PRs targeted against main branch labels May 28, 2025
@github-actions github-actions bot added the area: framework Framework is a tag for issues involving the developer framework. Eg Aqueduct label May 28, 2025
@Josmithr Josmithr requested a review from CraigMacomber June 10, 2025 20:36
Comment on lines +570 to +578
const index =
typeof propertyKey === "number"
? propertyKey
: asIndex(propertyKey, Number.POSITIVE_INFINITY);

// If the key is not a valid index, then there is no corresponding child.
if (index === undefined) {
return undefined;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear to me that we should be tolerating strings containing numbers to be used as keys into Arays here.

From reading our APi docs and types, I would not expect this to be supported.

Suggested change
const index =
typeof propertyKey === "number"
? propertyKey
: asIndex(propertyKey, Number.POSITIVE_INFINITY);
// If the key is not a valid index, then there is no corresponding child.
if (index === undefined) {
return undefined;
}
// If the key is not a valid index, then there is no corresponding child.
if (typeof propertyKey !== "number") {
return undefined;
}

Copy link
Contributor Author

@Josmithr Josmithr Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CraigMacomber I can remove this support, but I'm wondering how consistent we've been in this regard. ArrayNode's get explicitly does support this. Seems odd to me that we wouldn't support it here.

Copy link
Contributor Author

@Josmithr Josmithr Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I don't think that's what we want. POJO arrays allow both (implicitly numbers), and POJO objects allow both (implicitly strings), so it seems like we should follow suit.

https://www.typescriptlang.org/play/?#code/MYewdgzgLgBAhgJwXAngLhmArgWwEYCmCA2gLowC8MxAjADQBMdAzKQNwBQHokIANgQB0fEAHMAFImQpiABlIBKNjAD0KmDW7gI-ISIlTUxAESzji5Wo1ctkWADMQISjADeHGJ5iyMxgBIEfCLGdBwAvpy2OgLCYuKOIHIWqur+gcFRurESCSZmyVZpQSDGNkA

Maps are an exception here. And maybe we want to only allow string keys for those. But since we strictly support string keys in our maps, matching the policy for Objects may make more sense.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we just preserve current behavior? I think the current behavior is that numbers are allowed for arrays, but not for objects and maps.

Copy link
Contributor Author

@Josmithr Josmithr Jun 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that number keys are allowed when accessing Object node properties (though not Map node properties). E.g.

class TestObject extends schema.object("TestObject", {
	"0": schema.string,
}) {}
const test = new TestObject({
	0: "Hello world!"
});

const zero = test[0]; // "Hello world!"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, what I have is consistent with our standard property access for Arrays and Objects. And this is consistent with POJO. So, I'm pretty strongly convinced that deviating from this would be a bad idea.

With the exception of Map nodes. I think there is room for longer term discussion about maybe allowing use of numeric keys with our maps, since they strictly support string keys. But for now, that isn't supported, so it makes the most sense to be consistent with that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is literally impossible to implement arrays without supporting string keys, since JavaScript does not support numeric keys: they are stringified. The property key received by a proxy can never be a number: it is always a string, even when the user provides a number.

Our arrays support string keys exactly the same as JavaScript arrays, meaning we use typescript to tell you to pass in a number, which JS converts to a string behind the scenes. We do not support strings in places where not supporting strings is possible, for example in "at".

Our code for converting string indexes back to numbers exists to make indexing with numbers work: the fact indexing with strings works is an unfortunate side effect of this very questionable language.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example ["x"]["0"] gives "x" just like ["x"][0] does, and the proxy can't tell those two lookups apart as both search for a member under the key "0". Note that this also gives "x": {"0": "x"}[0] : this conversion from number to string is a JS property thing, and has nothing to do with arrays.

Copy link
Contributor Author

@Josmithr Josmithr Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my main point can be summed up by the following:

Array nodes can be indexed using string-formatted integer keys, so I don't think it makes sense for them not to work via child.

class MyArray extends schemaFactory.array("my-array", schemaFactory.string) {}
const myArray = new MyArray(["Hello world"]);
myArray["0"]; // "Hello world"
Tree.child(myArray, "0"); // Should match
  • Similarly, for object nodes, we support indexing them with numeric keys, so the behavior of child should match.
class MyObject extends schema.object("MyObject", {
	"1": SchemaFactory.optional(schema.string),
}) {}
const myObject = new MyObject({ 1: "Hello world" });

myObject[1]; // "Hello world"
TreeAlpha.child(myObject, 1); // Should match

Maps are a different story, and child already rejects number keys for them. But I think we need the above support for Arrays and Objects (and soon, Records).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, @Josmithr and I talked offline and I think the idea of consistency with POJO is compelling. I strongly disagree with the idea of possibly wanting maps to support something similar, as I think a very important property of maps is that keys that compare equal should look up the same value, and keys that do not should not. If we break that contract, I think this would be a massive bug pit, and I don't see any offsetting reason to do so. Setting that aside, I'll vote in favor of consistency with POJO :)

Copy link
Contributor

@alexvy86 alexvy86 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving for docs

Copy link
Contributor

🔗 No broken links found! ✅

Your attention to detail is admirable.

linkcheck output


> fluid-framework-docs-site@0.0.0 ci:check-links /home/runner/work/FluidFramework/FluidFramework/docs
> start-server-and-test "npm run serve -- --no-open" 3000 check-links

1: starting server using command "npm run serve -- --no-open"
and when url "[ 'http://127.0.0.1:3000' ]" is responding with HTTP status code 200
running tests using command "npm run check-links"


> fluid-framework-docs-site@0.0.0 serve
> docusaurus serve --no-open

[SUCCESS] Serving "build" directory at: http://localhost:3000/

> fluid-framework-docs-site@0.0.0 check-links
> linkcheck http://localhost:3000 --skip-file skipped-urls.txt

Crawling...

Stats:
  225443 links
    1710 destination URLs
    1941 URLs ignored
       0 warnings
       0 errors


@Josmithr Josmithr merged commit 87941b7 into microsoft:main Jun 17, 2025
38 checks passed
@Josmithr Josmithr deleted the tree/generic-traversal branch June 17, 2025 22:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: dds: tree area: dds Issues related to distributed data structures area: framework Framework is a tag for issues involving the developer framework. Eg Aqueduct base: main PRs targeted against main branch changeset-present public api change Changes to a public API
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants